# -*- coding: utf-8 -*-


# Copyright 2009 Larry Resnik, Lasfraka-Elixir

#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import with_statement
from __future__ import division

import codecs
from pyglet.window import key

def get_key_name(key_value):
    """Gets a comprehendable name for a key's value.

    If the key does not have a name, a string of the passed value is
        returned.

    :rtype: string
    """

    try:
        return key.symbol_string(key_value)
    except AttributeError:
        return str(key_value)

def get_key_value(key_name):
    """Gets the numerical equivalent of key_name.

    If it's not known and key_name is an int, a unique key is created.
    If it's not known and key_name is not an int, an AttributeError is
        thrown.

    :rtype: integer
    :return: A value mapping to a key.
    """

    try:
        return getattr(key, str(key_name))
    except AttributeError:
        try:
            return key.user_key(int(key_name))
        except ValueError:
            raise ValueError, u'Unknown key name: %s' % key_name

class KeyWatcher(object):
    """Class for remembering a key's state.

    :properties:
    name        A string for what this key elicits the action of.
    mapping     An integer of its key value assignment on the keyboard.
    down        A boolean representing if the button is held down.
    held        An integer counting how many update cycles the button has
            been held down. It resets to 0 when the key is released.
    prevHeld    An integer archiving the duration of the previous hold time.
    drought     An integer counting how many update cycles the button has
            been left untouched. It resets to 0 when pressed.
    prevDrought
                An integer archiving the duration of the previous drought.

    Attempting to compare a KeyWatcher object will compare its mapping.
    """

    def __init__(self, name, mapping):
        self.name = name
        self.mapping = mapping
        self.down = False
        self.held = 0
        self.prevHeld = 0
        self.prevDrought = 0
        self.drought = 0

    def __eq__(self, other):
        return self.mapping == other

    def __ne__(self, other):
        return self.mapping != other

    def is_double_tap(self):
        """Returns if this key was tapped twice in quick succession.

        Prior to tapping, the button must not have been held down for 10 or
        more update cycles. Also, the drought time of the current shot must be
        under 10 update cycles. When these are True, this method returns True
        so long as the key is held down.

        :rtype: boolean
        """
        return self.down and self.drought < 10 and self.prevHeld < 10

    def set_pressed(self, pressed):
        self.down = pressed
        if pressed:
            self.prevDrought = self.drought
        else:
            self.prevHeld = self.held
        self.held = 0
        self.drought = 0

    def update(self):
        if self.down:
            self.held += 1
        else:
            self.drought += 1

class KeyWatchManager(object):
    """Class for managing multiple KeyWatchers.

    Each key becomes a member variable. They may be assigned by passing a dict.
    All valid keys will become member variables for simplified calling.

    :parameters:
    key_list    An unsorted list of KeyWatchers. Do not sort. It does not
            contain certain keys (select, cancel, and skip).
    key_index_lookup
                A dict whose pairs are
            { numerical key assignment : index of key_list }.
            It is used to fetch the key_list index for each key press.
            It has and lacks the same things as key_list.
    DEFAULT_KEY_DICT
                A dict whose pairs are { key action : pyglet key name }.
            It is used to ensure proper initialization of the key_list.
            It references all keys in the game. It can be used as a
            base to pass to this class' initialiser.
    """

    DEFAULT_KEY_DICT = {'up':'UP', 'down':'DOWN', 'left':'LEFT',
        'right':'RIGHT', 'shoot':'C', 'jump':'X', 'dash':'Z',
        'special':'V', 'statuspause':'SPACE', 'gamepause':'ENTER',
        'select':'Z', 'cancel':'X', 'skip':'X'}

    def __init__(self, key_dict=None):
        """Initialize the KeyWatchManager. Sets the keys as member variables.

        key_assign_dict has pairs of
        { key action : string of key name or userkey }.
        If it is not set, a default set of keys are used.
        """

        self.key_index_lookup = {}
        self.key_list = []
        # The lookup dict stores numerical key assignments as dict keys, so
        # remembering the already assigned key names must be done seperately.
        assigned_keys = []
        if key_dict is None:
            key_dict = dict(self.DEFAULT_KEY_DICT)
        for key_action, key_assign in key_dict.iteritems():
            if key_action not in self.DEFAULT_KEY_DICT:
                continue

            try:
                assign_value = get_key_value(key_assign)
            except ValueError:
                assign_value = get_key_value(
                    self.DEFAULT_KEY_DICT[key_action])

            if key_action in ['select', 'cancel', 'skip']:
                # Don't add these to the list of keys so that they can have
                # the same key as any of the gameplay buttons. Otherwise, the
                # lookup dict would need to own a list of assignments for each
                # value. Because a dict can't have multiple of the same value,
                # we can store it once and not worry about overwritting it.
                assigned_keys.append(key_action)
                setattr(self, key_action, KeyWatcher(key_action, assign_value))
                continue

            if assign_value in self.key_index_lookup:
                raise KeyError, \
                    'Key %s was assigned multiple times.' % key_assign
            else:
                self.key_index_lookup.update(dict([[assign_value,
                    len(self.key_list)]]))
            assigned_keys.append(key_action)
            new_watch = KeyWatcher(key_action, assign_value)
            self.key_list.append(new_watch)
            setattr(self, key_action, new_watch)

        # Give the defaults to unset ones.
        for key in self.DEFAULT_KEY_DICT:
            assign_value = get_key_value(self.DEFAULT_KEY_DICT[key])
            if key not in assigned_keys:
#                print('%s was missing' % key)
                new_watch = KeyWatcher(key, assign_value)
                setattr(self, key, new_watch)
                if key not in ['select', 'cancel', 'skip']:
                    if assign_value in self.key_index_lookup:
                        raise KeyError, \
"""%s has multiple key assignments because a default assignment tried to take
the position of an unfilled key field (%s).""" % (key_assign, key)
                    self.key_index_lookup.update(dict([[assign_value,
                        len(self.key_list)]]))
                    self.key_list.append(new_watch)

        if self.select == self.cancel:
            raise KeyError, \
                'The select and cancel keys were given the same assignment.'

    def set_pressed_keys(self, pressed, symbol, modifiers):
        """Sets the state of any related keys.
        If the key is assigned to the gameplay-related keys and any of skip,
        select, or cancel keys, then all of the keys assigned to symbol are
        affected.

        :parameters:
        pressed         A boolean representing if a key was pressed down or
                    released.
        symbol          An integer. The key code.
        modifiers       Something from pyglet. The modifier keys that are in
                    effect while symbol's state was altered.
        """

        if symbol in self.key_index_lookup:
            self.key_list[self.key_index_lookup[symbol]].set_pressed(pressed)
        if symbol == self.skip:
            self.skip.set_pressed(pressed)
        if symbol == self.select:
            self.select.set_pressed(pressed)
        elif symbol == self.cancel:
            self.cancel.set_pressed(pressed)

    def on_key_press(self, symbol, modifiers):
        self.set_pressed_keys(True, symbol, modifiers)

    def on_key_release(self, symbol, modifiers):
        self.set_pressed_keys(False, symbol, modifiers)

    def update(self, dt=0.0):
        for key in self.key_list:
            key.update()
        self.select.update()
        self.skip.update()
        self.cancel.update()

class Settings(object):
    """This class manages the data for the config file.

    The stuff that can be read and written with files are in the properties
    dict. The following are valid properties:

    config_version  A string for the version of the configuration file's
                set-up. Please leave this value for the game to read. The
                default value may vary between game versions and will not be
                affected by the version that is read-in.
    bgm_volume      An integer for the background music's volume. Must be
                between 0 and 100. A number outside that range will be
                set to the extreme it is closer to. Default is 100.
    debug           A boolean that toggles displaying debugging information.
                Default is False.
    frail           A boolean that, when True, causes the player to begin
                each game with one health. Default is False.
    fps             A boolean that toggles displaying the update speed of the
                game. Default is True.
    fullscreen      A boolean that toggles between fullscreen and windowed
                mode. Not well-supported right now. Defaults to False.
    invincible      A boolean that toggles the player's invincibility. For
                debugging only. Defaults to False.
    sfx_volume      An integer for the sound effects' volume. Follows the same
                rules as bgm_volume.
    scale           An integer for the size ratio of the window. 100 is normal.
                200 is double-sized. Deafault is 100.
    vsync           A boolean that toggles the use of vsync. Default is True.
    """

    CONFIG_VERSION = '1.0.1'

    def __init__(self):
        # Non-properties options.
        self.framerate = 60.0
        self.preload_scenes = False
        self.character = 'marisa'
        self.key_watch_manager = None
        self.key_assign_names = {'up':'UP', 'down':'DOWN', 'left':'LEFT',
                     'right':'RIGHT', 'shoot':'C', 'jump':'X', 'dash':'Z',
                     'special':'V', 'statuspause':'SPACE', 'gamepause':'ENTER',
                     'select':'Z', 'cancel':'X', 'skip':'X'}

        # Only the items in properties get written to a file.
        self.properties = {
            'config_version':self.CONFIG_VERSION,
            'bgm_volume':100,
            'debug':False,
            'frail':False,
            'fps':True,
            'sfx_volume':100,
            'scale':100,
            'fullscreen':False,
            'vsync':True,
            'invincibility':False
        }
        self.write_order = ['bgm_volume', 'sfx_volume', 'scale', 'fullscreen',
            'vsync', 'fps', 'invincibility', 'debug', 'frail']
        self.write_order_key = ['up', 'down', 'left', 'right', 'jump', 'shoot',
            'special', 'dash', 'statuspause', 'gamepause', 'select', 'cancel',
            'skip']

    def get_keys_as_values(self, key_dict=None):
        """Returns a dict of the registered keys with their value mappings.

        key_dict has a key referring to the button action and a value that
            either describes the button on the keyboard or is a string of the
            user-defined key. When not set, this class' own key dict is used.

        :rtype: dict
        :return: A dict with {button action string : numerical mapping} pairs.
        """

        if key_dict is None:
            key_dict = self.key_assign_names
        mylist = []
        for key_action, assign_name in key_dict.iteritems():
            mylist.append((key_action, get_key_value(assign_name)))
        return dict(mylist)

    def get_keys_for_game(self, key_dict=None):
        """Returns a dict of the registered keys with their value mappings.

        key_dict has a key referring to the button action and a value that
            either describes the button on the keyboard or is a string of the
            user-defined key. When not set, this class' own key dict is used.

        :rtype: dict
        :return: A dict with {button action string : numerical mapping} pairs.
        """

        if key_dict is None:
            key_dict = self.key_assign_names
        mylist = []
        for key_action, assign_name in key_dict.iteritems():
            mylist.append((key_action, get_key_value(assign_name)))
        return dict(mylist)

    def _set_option(self, option, value):
        """Sets a property safely.

        Bad values are either ignored or set to something more reasonable.

        :rtype: bool
        :return: True if a property was assigned. False otherwise.
        """

        if option in ['bgm_volume', 'sfx_volume'] and value.isdigit:
            # Load as a float just in case. Then truncate to int.
            self.properties[option] = max(min(int(float(value)), 100), 0)
        elif option == 'scale' and value.isdigit:
            self.properties[option] = max(min(int(float(value)), 250), 50)
        elif option == 'config_version':
            self.properties['config_version'] = value
        elif option in ['fps', 'fullscreen', 'vsync', 'invincibility',
                        'debug', 'frail']:
            lowerString = value.lower()
            if lowerString in ['true', '1', 'on', 'yes']:
                self.properties[option] = True
            elif lowerString in ['false', '0', 'off', 'no']:
                self.properties[option] = False
            else:
                return False
        else:
            return False
        return True

    def _scrutinize_line(self, line, key_dict):
        """Parse a line of data so it can be interpreted.

        Keys are set to lower-case and values are set to upper-case for
            keyboard mappings.
        Lines that start with # or ; are ignored.

        :rtype: bool
        :return: True if an assignment was successful. False if an assignment
            failed or no assignment took place.
        """

        if line.startswith('#') or line.startswith(';'):
            return False
        try:
            stringKey, stringValue = line.split('=', 1)
        except ValueError:
            # There's not a single '=' in that line.
            return False
        stringKey = stringKey.strip()
        stringValue = stringValue.strip()
        if stringKey.lower() in self.properties:
            return self._set_option(stringKey.lower(), stringValue)
        elif stringKey.lower() in self.key_assign_names:
            if stringValue.upper() == 'ESCAPE':
                return False
            try:
                # If this is convertable, we will use it.
                get_key_value(stringValue.upper())
            except ValueError:
                return False
            key_dict[stringKey.lower()] = stringValue.upper()
            return True
        return False

    def read(self, filename='config.txt'):
        """Read the configuration file and set any properties it mentions.

        Each property-setting pair must be separated by an equals ( = ) sign.
        All properties and settings are case-sensitive. Invalid properties or
        settings will be ignored. Properties may be assigned in any order.
        The file is read in UTF-8 format. If the file can't be opened or
        doesn't exist, no settings are set.

        See the class docstring for valid properties.

        :rtype: boolean
        :return: True if all IO actions succeded.
        """

        successful = True
        key_dict = dict(self.key_assign_names)
        try:
            with codecs.open(filename, 'r', 'utf-8') as configFile:
                for line in configFile:
                    self._scrutinize_line(line, key_dict)
        except IOError:
            successful = False

        try:
            self.key_watch_manager = KeyWatchManager(key_dict)
        except KeyError, msg:
            print(msg)
            print('Now using the default key assignment setup.')
            self.key_watch_manager = KeyWatchManager(self.key_assign_names)
        return successful

    def write(self, filename='config.txt'):
        """Write the configuration file.

        The file is written in UTF-8 format. The options are alphabetized.

        :rtype: bool
        :return: True if all IO actions succeded.
        """

        key_manager = self.key_watch_manager
        try:
            with codecs.open(filename, 'w', 'utf-8') as configFile:
                for opt in self.write_order:
                    configFile.write(''.join((opt, '=',
                        str(self.properties[opt]), '\n')))
                configFile.write('\n')
                # TODO: Probably want to put this in a KeyWatchManager method.
                for key in key_manager.key_list:
                    configFile.write(''.join((key.name, '=',
                        get_key_name(key.mapping), '\n')))
                configFile.write(''.join(('select', '=',
                    get_key_name(key_manager.select.mapping), '\n')))
                configFile.write(''.join(('cancel', '=',
                    get_key_name(key_manager.cancel.mapping), '\n')))
                configFile.write(''.join(('skip', '=',
                    get_key_name(key_manager.skip.mapping), '\n')))
#                for opt in self.write_order_key:
#                    configFile.write(''.join((opt, '=',
#                        self.key_assign_names[opt], '\n')))
                configFile.write(''.join(('\n', 'config_version', '=',
                    str(self.CONFIG_VERSION), '\n')))
        except IOError:
            return False
        return True
